Feat/sigma renderer#2
Merged
Merged
Conversation
Phase 0+1 of the full G6→Sigma cutover per MIGRATION.md: - vendor sigma@3 + graphology + bubblesets-js as two esbuild bundles (graphology node-safe for vitest, sigma browser-only, 267 kB combined) - add deterministic 6000n/9000e perf harness (npm run perf) encoding the MIGRATION.md acceptance gates; all five gates pass (load 729 ms, first-interaction stall 65 ms, wheel-zoom 60 FPS, 500-node select 43 ms) - new graph_model.js: graphology population + pure node/edge reducers - new sigma_adapter.js: transitional G6-shaped facade (sole sigma importer) - core.js: replace G6 instance/event choreography with sequential sigma lifecycle; delete G6#6373 zoom workaround and dead event locks - filter.js: port visibility to graphology hidden-attr diffing - tests: 645 passing (29 new for model/reducers/filter; 9 G6-source-pinning tests removed); review fixes from code-reviewer + js-reviewer passes Interactions, shapes/labels parity, bubble sets and layout parity follow in phases 2-5; g6.min.js removal in phase 6.
Phase 2 of the G6→Sigma cutover per MIGRATION.md: - node shapes: circle/square native; bordered circles/rects and diamond/hexagon/triangle/star via crisp SVG-texture programs (@sigma/node-image, new shape_textures.js); paint values validated against an SVG-safe allowlist before embedding, texture cache capped - edges: straight/curved x arrow/sourceArrow/doubleArrow program matrix; polyline degrades to curve and lineDash is not rendered (documented in API.md; values still round-trip) - labels: custom node/edge drawers (label_renderers.js) honoring per-element size/color/background/placement/offset/auto-rotate; CFG.HIDE_LABELS synced live with explicit labels kept visible (G6 semantics) - states: halo treatment per old G6 spec, colors centralized in DEFAULTS.STATE (config.js); selected > highlight > dim precedence - y-axis: app model stays y-down, flipped exactly once at the graphology boundary so legacy saved layouts keep their orientation (G6 size diameter → sigma radius likewise mapped) - tests: 700 passing (Excel style round-trip, shape textures incl. injection guards, label drawers incl. vertical-edge rotation boundary) All five perf gates still pass (load 831 ms, stall 65 ms, wheel 60 FPS, 500-node select 62 ms).
Phase 3 of the G6→Sigma cutover per MIGRATION.md — replaces the G6 behaviors/plugins with explicit interaction wiring (new interactions.js, node-safe lasso_geometry.js): - node drag: downNode + captor mousemove → viewportToGraph, camera pinned via custom bbox; positions persisted on mouseup (serialized so overlapping drag-ends cannot race), no y double-flip (verified in-browser) - selection: click / shift-toggle / canvas-deselect routed through GraphSelectionManager so undo/redo, data table and buttons stay in sync; drag-ending clicks suppressed - hover-activate: 1-degree neighborhood via a dedicated hoverIds Set so hover can never corrupt selection; CFG.DISABLE_HOVER_EFFECT checked live - lasso: freehand canvas overlay + even-odd point-in-polygon over visible node viewport coords; shift adds; Esc/pointercancel clean up - tooltip: DOM tooltip fed from cache.toolTips, now sanitized with DOMPurify at the display boundary (labels/descriptions from loaded files are untrusted; XSS payload verified inert in-browser); expand/close buttons moved from inline onclick to a delegated listener - async sigma event handlers wrapped so rejections surface via ui.error instead of vanishing unhandled - deleted: adapter behavior/event/plugin stubs, core.js BEHAVIOURS stubs, all APPLY_BUBBLE_SET_HOTFIX paths incl. the CFG flag Tests 700 → 724 (lasso geometry incl. boundary/self-intersection pinning, hover layer); all five perf gates pass (load 862 ms, stall 66 ms, wheel 60 FPS, select 40 ms).
Phase 4 of the G6→Sigma cutover per MIGRATION.md — bubble sets, PNG export and minimap move onto owned canvas layers: - bubble sets: one canvas under sigma's edge/node layers (new bubble_layer.js + node-safe bubble_geometry.js); bubblesets-js outlines computed from viewport rects, cached in graph space and reprojected on pan (zoom drift >0.3 log2 refits after a 150 ms settle); membership/ style keys stored at update time so the per-frame cost is one O(n) integer-hash position checksum; GraphBubbleSetManager call-surface unchanged (filter sync, manual groups, style UI, group labels all verified in-browser); the G6 #7195 path-churn bug class is gone - avoid-members: threshold raised 300→1000 backed by measurements (69 ms at 1000 avoid rects, 12 s cliff at 5700 — documented in config) - PNG export: adapter.toDataURL composites the bubble layer under the @sigma/export-image render (viewport-aligned at any DPR); bitmap closed and errors rethrown descriptively - minimap: owned thumbnail (minimap.js — dots, viewport rect, click/drag pan, bottom-right as shipped), replaces the G6 plugin and .g6-minimap CSS - review fixes: order-sensitive integer position hash (float-accumulation collisions at large coords), content-keyed avoid members, save/restore bracketing per group draw, defensive canvas removal on destroy, NUL-joined id keys, JSON-stringified style keys Tests 724 → 748; all five perf gates pass (load 765 ms, stall 62 ms, wheel 59.8 FPS, select 43 ms); bubble probe: 300-member group recompute 47 ms cold / 14 ms membership, 59.8 FPS wheel with overlays active.
- extract layout execution from sigma_adapter into node-safe src/graph/layout_algorithms.js (circular/grid/forceAtlas2 moved verbatim) - add radial/concentric/mds layouts via headless @antv/layout v2 (execute + forEachNode readback, finite-coordinate guard, instance destroy) - vendor @antv/layout into graphology.bundle.mjs (108 -> 207 kB) - add 27 requirement-driven tests covering all 6 layout types, edge cases (empty/single-node/disconnected) and fallback behavior
- delete vendored g6.min.js (1.1 MB) and its script tag; distribution shrinks ~0.9 MB net - fix guided tour minimap selector (.g6-minimap -> .gll-minimap) - trim dead LAYOUT_INTERNALS options (force/circular/grid take none) and drop commented-out fruchterman/antv-dagre entries - rewrite stale G6 comments (gll.js, api_client.js, io.js, server/static.js, vendor_libs.js) - README: drop the fixed antvis/G6#7195 known issue - THIRD_PARTY_NOTICES: add bundled runtime deps (sigma+plugins, graphology+utils, @antv/layout, bubblesets-js, marked, dompurify) - CHANGELOG 1.15.0 with measured perf table; MIGRATION.md marked complete - bump version to 1.15.0 (config injected) All 775 tests green; perf gates pass (load 785 ms, zoom 59.7 fps, stall 64 ms, select 46 ms).
- updateManualGroupStatus mixed && / || without parentheses, so filtered-out or stale manual members could still count toward the status badge - remove the dead INVISIBLE_DUMMY_NODE config and its guard conditions in bubble_sets.js — nothing adds the dummy node since the sigma cutover, and the nodeRef/current-graph checks subsume every guard - document the bubble label placement/close-to-path/auto-rotate no-op knobs in the 1.15.0 changelog entry
- dragging a selected node now moves the whole selection (per-frame delta applied to every selected id; cache.selectedNodes normalized Set/Array) - lasso overlay canvas z-index 1000 -> 900 so the toolbar (z 1000) stays clickable and the lasso icon can disable lasso mode - dragged node's label pinned via forceLabel for the drag duration so sigma's label grid can't drop or reassign it mid-drag
…tainer resize - force TEXTURE_MIN_FILTER=LINEAR after every atlas bindTextures: ANGLE/radeonsi intermittently fails generateMipmap on NPOT atlas re-uploads, leaving textures mipmap-incomplete and sampled opaque black (the hover black boxes after restyling) - atlas regeneration debounce 500 -> 50 ms so restyled nodes aren't invisible (transparent base color) while their new textures load - hover drawer gated during node drag so passed-over nodes can't pop their hover label (square program wraps its instance drawHover; shape/circle go through defaultDrawNodeHover) - debounced ResizeObserver on the graph container replaces the five race-prone setTimeout resize workarounds in ui.js; sigma v3 only auto-resizes on window resize, so panel toggles left a stale viewport until the next zoom/pan
…, empty-query AST - hotkey/global event registration guarded by module-scope flags: Cache.reset() recreated EVENT_LOCKS on every file load, stacking anonymous document listeners (even counts made the lasso hotkey a no-op) - createGraphInstance re-applies per-layout node/edge styles when the layout carries any (lost in fd7e342: G6's createSimplifiedDataForGraphObject merged them, buildGraphologyGraph doesn't) — fixes data-editor apply and JSON load rendering unstyled until a manual workspace switch; skipped when no custom styles exist (the reset pass costs ~700 ms at 15k elements) - updateMetricUI also proceeds when the selected metric has no cached values: under sigma fresh elements report 'visible', the visibility diff is empty, and visibleElementsChanged never fired on load, so metrics silently never computed - empty query AST now matches everything (no constraints) instead of nothing - new tests: query AST empty/malformed semantics, metrics gate (jsdom, real centrality calculations)
…e-to-path/auto-rotate - bubblesets-js influence radii were fixed pixel constants: zoomed out, non-member avoid discs cancelled the members' energy field and the outline collapsed to an empty path, vanishing the group. Field constants now scale with sigma's node-size factor (1/sqrt(camera.ratio)), with a one-shot no-avoid retry as safety net and the ratio bucket in the outline cache key so empty results aren't sticky - settle-recompute machinery removed (dead once recompute keys on ratio buckets) - labelPlacement (top/bottom/left/right/center), labelCloseToPath and labelAutoRotate style knobs now work: outlineLabelAnchor returns anchor + tangent angle + outward normal per placement; detached labels stand off along the normal; auto-rotate aligns text to the outline tangent. Default placement 'bottom' restores the old G6 look (labels move from the hardcoded top)
- badge data (badge/badges/badgePalette/badgeFontSize) flowed through the style pipeline but was never mapped to sigma attributes nor drawn — badges now render as G6-parity colored pills with white text on the node perimeter (all 12 placements) - badge_clear emits explicit badge:false + empty arrays so mergeNodeAttributes can't leave stale badges - texture->native program transitions clear stale image/fillColor attrs (wrong hover state-texture colors) and restore the fill color - v1 limitation: badges draw in the label pass, so they follow label visibility/thinning
- export FA2Layout from the node-safe graphology vendor bundle (worker module only touches Worker/window.URL at instantiation) - executeLayout force branch: where Worker exists, run the FA2 worker supervisor for a bounded time budget (min(5000, 500 + 2*order) ms) so the layout animates live without blocking the main thread; stop()+kill() in finally; per-graph re-entrancy guard - node (vitest) keeps the deterministic synchronous forceAtlas2.assign path unchanged; test seam via optional ForceSupervisor override - 5 new tests (818 total green); perf gates unaffected (benchmark fixture carries persisted positions, force never runs during timed load)
- add graphology-metrics@2.4.0 to the node-safe graphology vendor bundle - build a temp undirected multigraph from the visible subgraph and run library betweenness/closeness/eigenvector/pagerank/degree on it; result contract (scores/nodeValues/graphLevelMetrics/popups) preserved - value fixes vs hand-rolled: eigenvector now truly converges on bipartite graphs ((I+A)x iteration), Graph Density genuinely computed, n<=2 betweenness no longer NaN - new graph-level metrics: density (degree panel), diameter (closeness panel, '∞ (disconnected)' when infinite) - eigenvector non-convergence caught and surfaced via ui.error; loading overlay protected by try/finally in updateMetricUI/ensureMetricValues - review fixes: uniform n<=1 early exit in all five calculators, max||1 guard against '(NaN %)' score texts, self-loop degree semantics pinned - metrics.js 963→~800 LOC; tests/metrics.test.js 30→58 tests (846 total green); perf gates green
- add graphology-communities-louvain@2.0.2 to the node-safe vendor
bundle, plus modularity from graphology-metrics
- new node-safe src/graph/communities.js: detectCommunities builds the
visible subgraph, runs seeded louvain.detailed (mulberry32, fixed
seed → reproducible), buckets communities largest-first and maps the
top 4 onto the bubble group keys; null guard when no visible edges
- extract buildVisibleGraph from metrics.js into shared node-safe
src/graph/visible_graph.js (pure move, reused by metrics + communities)
- GraphBubbleSetManager.detectCommunities replaces the current layout's
four ${group}ManualMembers sets and reruns the existing manual-group
choreography; result toast reports community count + modularity
- 🧩 button in the selected-nodes row (matches neighboring inline-onclick
idiom)
- 8 new node-safe tests in tests/communities.test.js (854 total green);
perf gates green
- add graphology-layout-noverlap@0.4.2 to the node-safe vendor bundle - executeLayout strips spec.noverlap and runs exported applyNoverlap (margin 5, max 100 iterations, respects node size attr) after any base layout type; flag never leaks into @AntV options or LAYOUT_INTERNALS - GraphLayoutManager.removeNodeOverlaps() applies the pass to the live graphology instance for all nodes in the current workspace, persists positions via the existing getNodeData sync, triggers redraw - 'Remove overlaps' button in the workspace action row - 8 new tests across layout-algorithms/layout-selected-nodes (864 total green); perf gates green
- add @sigma/node-border@3.0.0 to the browser-only sigma vendor bundle - bordered circles render through a new borderCircle program (outer ring reads borderColor + relative borderRatio = min(lineWidth/radius, 1), inner fill ring consumes the rest) — crisp at all zooms, no texture atlas churn; non-circle shapes and bordered rects stay on the SVG texture program - nodeProgramAttributes is now a three-branch dispatch (texture / borderCircle / native) where every branch writes the full merge-hygiene attr set; native branch unconditionally clears borderColor/borderSize so lineWidth-only deltas can't leak a stale stroke into halo textures (review fix) - selection halos deliberately stay on the texture path for all shapes: a GLSL halo would duplicate the halo implementation for circles only and expand the state/program matrix; dim on bordered circles now takes the cheap native branch - no hover guard needed (program has no instance drawHover → falls through to guarded drawDiscNodeHover); PNG export inherits the program registry via sigma.getSettings() - 13 new attribute-level tests incl. 6-case transition merge matrix (877 total green); perf gates green
- new node-safe src/graph/webgl_support.js: webgl2 probe + persistent in-container error message; core.js imports SigmaAdapter lazily so the sigma bundle's module-scope WebGL probes can't kill app boot, probes before construction, and wraps it defensively — on failure cache.graph stays null (same state as 'no data loaded') and app chrome survives - createGraphInstance memoizes its in-flight promise (re-entrancy across the lazy-import await could construct two adapters, leaking a context) - silent null-graph no-op guards on the UI-reachable filter/selection entry points reachable when init failed - resize blank-graph fix: sigma.resize() clears the WebGL buffers but never schedules a render — the adapter facade resize() now calls sigma.scheduleRender() (render() resizes first); evidence + manual repro kept as scripts/resize_redraw_check.js - 24 new tests across webgl-support/graph-core-init/selection-null-graph/ filter-visibility (898 total green); perf gates green
- new src/graph/edge_programs.js: parametric SDF marker-head program (arrow/rect/diamond/circle/tee inhibition marker selected per edge end via float enum, per-end sizes in graph px, zero-area collapse for disabled ends), EdgeRectangleProgram-clone halo underdraw reading haloWidth/haloColor, curve halo via re-processed curve program - curved edges fully supported: marker heads orient along the quadratic-bezier tangent (edge-curve's baked arrowHead bypassed) - registry collapses to 4 keys: line/curve fast paths unchanged, styledLine/styledCurve compounds (halo → body → source head → target head); marker/halo are per-edge attributes so the vocabulary never grows the registry - start/end arrow size and type UI knobs now take effect; new marker vocabulary arrow/rect/diamond/circle/tee with legacy G6 names aliased at render time (files round-trip unchanged); Excel validators accept both; halo enable/color/width implemented; API.md updated - selection composes with user halos: post-reducer size widens line and halo together - 17 new attr-mapping/hygiene tests (915 total green); perf gates green (fixture edges route to the unchanged plain-line path); visual pixel probe script scripts/edge_markers_check.js 13/13
- new badgeScaleWithNode style prop (default off, preserves current behavior): zoom-independent factor nodeDiameter/DEFAULTS.NODE.SIZE is baked into badgeScaleFactor by graph_model so the renderer stays config-free; drawNodeBadges multiplies it into the badge font size - Badge Size slider (4-32, exposes the previously unreachable badgeFontSize) and Badge Scale With Node toggle in the node style card - badgeScaleWithNode normalized in style.js so it round-trips through Excel export like the other badge props - 6 new tests across graph-model/label-renderers (921 total green); perf gates green
- CSS custom-property tokens at the top of style.css (:root light set +
[data-theme="dark"] overrides, color-scheme flip for native widgets);
~230 hardcoded colors converted across panels, sidebars, inputs,
popups, tables, tooltips, overlays; brand purple/red kept as-is
- new src/utilities/theme.js: resolveInitialTheme (stored pref wins,
prefers-color-scheme as unstored default that keeps following the OS),
applyTheme via data-theme on <html> persisting to localStorage gllTheme
- 🌙/☀️ toggle button; renderer default label color flips live via
setSetting; stage/minimap/bubble canvases themed
- label readability: io.js bakes labelFill #000000 into every labelled
element, so exactly that baked default is treated as 'no explicit
choice' and follows the theme; any other user-set label color is
honored (deliberate trade-off: an explicit pure-#000000 label lights
up in dark mode)
- review fixes: hover-pill pins only the FALLBACK label color to #000
via the {attribute, color} form so explicit label colors survive
hover; adapter construction resolves the theme via idempotent
initTheme (no race with the DOMContentLoaded boot); toggleDarkMode
de-async'd
- 16 new tests (936 total green); perf gates green
- suppress sigma's hover pill layer while a drag is in flight: the dragged node is permanently hovered and its white disc + label pill (drawn on the hovers canvas, above the labels canvas) invisibly blanked every label it passed over on light backgrounds - pin all on-screen labels (forceLabel) for the duration of a node drag so the rebuilt label grid cannot evict neighbours' labels from cells the dragged node passes through; pins release on mouseup - tune labelGridCellSize to 50 (sigma default 100): ~4x more labels shown and smaller label-competition zones between nearby nodes - add drag interaction tests (label pinning, group drag, position persistence) with a stub sigma + real graphology
Replace the full-screen dimmed centered overlay with a compact status card docked top-center. - Drop the rgba(0,0,0,0.2) dimming; the full-screen layer stays at z-index 10001 to keep loading blocking, but is now transparent. - Top-center placement avoids the bottom-right minimap and the top-right selected-elements panel. - Inline 20px spinner beside a header + muted detail line; messages wrap (including long URLs) instead of truncating, so no info is lost. - Zero the .h7 margin on the detail text so it left-aligns under the header; add role=status/aria-live for screen-reader progress.
Extend edge end-markers with per-end fill color, border color, and border size, threaded through the full styling pipeline: - config: arrow color/border-color (null = inherit edge color / no border) and border-size (0 = auto, scales with the marker) defaults + palettes - style/graph_model: map the new style keys to start/end marker attrs, emitting the full set so a disable overwrites stale values - edge_programs: add a_borderColor + a_borderSize to the marker-head WebGL program; draw an SDF border band (explicit px, else ~20% proportional) - ui_style_div: color pickers + border-size sliders for start/end arrows - io: Excel columns for round-trip of the new arrow style props - tests: marker color/size mapping + extended round-trip coverage
The node/edge-count cutoff that auto-disabled hover highlighting was a G6-era guard; sigma renders hover fast enough that it is no longer needed. Hover is now controlled solely by the manual toggle button. - config: drop MAX_NODES/EDGES_BEFORE_DISABLING_HOVER_EFFECT (keep the flag) - io: stop deriving DISABLE_HOVER_EFFECT from graph size in preProcessData - tests: drop the assertion on the removed constant
The new arrow color/border controls (palette swatches + picker + hex input) wrapped to two lines at 360px. 420px keeps every row on a single line. The panel is a flexbox sibling, so the graph overlays (minimap, selection frame) and the tour popup follow the new width automatically.
Network metric calculations (betweenness, eigenvector, etc.) ran on every filter change, even when the metrics panel was closed and nobody was viewing them. Gate computation on panel visibility instead: - updateMetricUI() early-returns while the panel is collapsed - toggleUI() triggers a compute when the panel opens - invalidateMetricValues() blanks stale per-node tooltip metric text on every visibility change, skipping the tooltip scan when no metric text is active (metricTooltipsActive flag) - fix collapsed init (false -> true) to match the default-closed CSS Tooltips now show either the current correct value (panel open) or nothing (panel closed) -- never a stale value. Metric-driven styling is unchanged (already a snapshot taken at apply-time).
bubblesets-js sample(step) treats step as an array index stride, and PointPath.get() returns undefined for a fractional index. The outline sampler passed OUTLINE_SAMPLE_PX * scale directly, so any zoom whose 1/sqrt(ratio) field factor made 8*scale fractional (e.g. 2.83) poisoned the sampled path with undefined points and crashed bSplines()/simplify(). The throw escaped the bubble layer's rAF paint, freezing the bubble canvas: zoom appeared to ignore the camera and snapped back only at the original zoom, re-detecting communities or modifying a zoomed graph drew nothing. Ratio 1 (integer stride 8) is why it always worked first. - Round the stride to an integer >= 1 (Math.round). - Guard the bubblesets-js call so a degenerate outline returns [] per the function's documented contract instead of throwing into the renderer. - Add a faithful BubbleSetLayer harness reproducing the zoom/clear/re-add paths and a fractional-step geometry regression.
…tays selectable A fixed absolute slider step (1e-6) floored the reachable max below a column's true max, so the node holding the maximum value (e.g. ADCY1 in the AOP208 column) was excluded from BETWEEN filters and bubble-set assignment — selectable only via the query editor. - Float columns now use step="any" (continuous): the exact max and any high-decimal value are reachable via both slider and number box, at any column magnitude. - Integer columns keep step=1 (discrete counts). - Drop the now-unused FILTER_STEP_SIZE_FLOAT constant. - Tests updated; add constructor-wiring tests asserting integer->1 and float->"any" with the full-precision AOP208 max preserved.
The 🧩 button now opens a configurator instead of running immediately:
pick a numeric edge property to weight by (or topology-only) and the
Louvain resolution, then detect. Weighting lets community structure that
is invisible to pure topology (e.g. STRING confidence scores) drive the
grouping; resolution tunes community granularity.
- communities.js: detectCommunities(cache, groups, {weightProperty,
resolution}) -> getEdgeWeight + resolution. Fixed seed kept, so results
stay reproducible.
- visible_graph.js: optional {weightProperty} attaches a numeric weight
per edge from edge.featureValues (fallback 1 for missing/non-finite).
The metrics callers pass no options and are unchanged.
- bubble_sets.js: enumerate numeric edge props from filterDefaults, build
the popover, default to Combined Score when present (else topology),
report weight + resolution + modularity.
- Tests: weighting recovers structure invisible to topology, equal
weights match unweighted, missing-value fallback, resolution raises
community count, omitted resolution defaults to 1.
Optional per-edge flow overlay (source→target movement), styled like the existing edge halo and persisted with the graph data: - edge_flow_programs.js (new): EdgeFlowProgram — EdgeRectangleProgram clone drawing marching dashes or travelling pulse dots over the edge body in screen-px units (zoom-stable spacing); flowMode 0 collapses to zero fragments so non-flowing edges cost nothing - flow_animator.js (new): FlowAnimator — rAF loop advancing the shared flowClock and issuing redraw-only refreshes, active only while ≥1 visible edge flows and the tab is visible; graphology-event dirty flag, full teardown in destroy() - graph_model.js: FLOW_MODES, edgeFlowMode, lightenHexColor; per-edge flowMode/flowSpeed/flowColor attrs with the full-set invariant (off → 0/0/null so stale flow never survives a disable); flow routes edges to the styled compound programs - sigma_adapter.js: flow sub-program registered in styledLine after the body, before marker heads; animator lifecycle next to the bubble layer - ui_style_div.js: Flow on/off, type (dash|pulse), speed, color rows mirroring the halo block - style.js: flow keys added to the persisted-style whitelist so loaded files keep their flow styling - config.js: DEFAULTS.EDGE.FLOW + EDGE_FLOW_TYPES vocabulary Curved edges route to styledCurve but draw no overlay yet (curve-shader fork lands separately). Tests: +11 (1085 total expected).
Fork the bundled @sigma/edge-curve shaders so curved edges animate the same dash/pulse flow as straight ones: - edge_flow_glsl.js (new, node-safe): pure string patchers over the bundled GLSL — getDistanceVector/distToQuadraticBezierCurve rewritten to expose the closest-point bezier parameter t (out param), flow varyings + u_time injected, body output swapped for the dash/pulse mask, vertex gains a_flow/a_flowSpeed + flow-off collapse + an 8-segment projected arc-length varying (CSS px) that t scales by. Every patch anchors on exact bundle substrings and throws on drift; explicit stage assertions reject wrong-stage sources. - edge_flow_programs.js: createCurveFlowProgram — patches the parent's shaders eagerly at class-creation time, appends the two float attrs after the computed parent stride, rides the parent's a_color slot for the flow color (CPU-side substitution, like the curve halo). - sigma_adapter.js: registered in the styledCurve compound between body and marker heads inside try/catch — anchor drift after a sigma upgrade degrades to non-animated curves with a console.warn, straight-edge flow unaffected. Pattern constants are duplicated across the straight program and the patchers (GLSL stages can't share consts); a co-change test parses both sources and fails on numeric drift. 19 new patcher tests run against byte-verbatim bundle fixture snapshots (1103 total).
Atmospheric 2d canvas layer below the edge layer (deepest of the beforeLayer: "edges" stack — earliest-created sits deepest, verified against sigma's createLayer), with two independent off-by-default passes toggled from the workspace toolbar (🌡️ / 🔆, hover-toggle conventions): - HEATMAP: per-node gaussian alpha splats stamped into an offscreen canvas in graph-space px, colored through a ramp LUT, cached under a viewport-INDEPENDENT key (positions checksum + serialized ramp stops + bandwidth/resolution config) — pan/zoom never re-splats; each frame is one drawImage under the camera affine. Light/dark ramps in config; theme flips recolor via the key. - GLOW: accent radial gradients behind selected visible nodes in viewport space; selected node positions ride the redraw signature so dragging a selected node moves its glow. heatmap_geometry.js (node-safe) carries the pure math — bbox, splat transform with degenerate-bbox guards, auto bandwidth (diagonal/√n, clamped), validated hex parsing, ramp LUT build/apply. destroy() releases the offscreen's backing store eagerly (workspace switches replace the adapter). 31 new tests (1134 total).
…rry bubbles, blank 8x exports) by re-fitting and re-painting instead of bitmap-stretching the on-screen canvas and by respecting the GPU's WebGL framebuffer instead of 2D-canvas constants
# Conflicts: # src/graph/sigma_adapter.js
…ap in styling panel - remove the selection-glow pass (redundant with the reducers' accent halo ring): GLOW config, layer pass, and toolbar button are gone - add runtime heatmap settings on the layer (intensity, opacity, radius scale, gamma/contrast, density threshold, ramp preset, dim-graph) with NaN-safe validation; field-shaping knobs ride the offscreen cache key - wire the previously unused gamma exponent into the ramp (default 0.7 boosts low-density haze) and add a density threshold that clears sub-floor pixels and renormalizes survivors, so a floor just above the splat intensity shows only overlapping nodes (clusters), not singles - replace RAMP_LIGHT/RAMP_DARK with five theme-aware ramp presets (default, viridis, magma, accent, grayscale); resolved stops ride the cache key so ramp switches and theme flips recolor correctly - move all controls from the toolbar (🌡️/🎛️ buttons removed) into a collapsible Density Heatmap card in the styling panel, next to the workspace-level Bubble Sets card; sliders update live while dragging - add a dim-graph companion: stateless nodes/edges render dimmed while the heatmap is on so the field reads through; selection/hover/explicit states keep their normal treatment - close anchored popovers on graph teardown (closeAnchoredPopovers in destroyGraphAndRollBackUI) so outside-click document listeners and the destroyed adapter are never referenced by a stale popover
…y controls - two new flowType patterns in both WebGL programs (straight + curved GLSL fork): comet (sharp head, quadratic fading tail) and chevron (cross-axis-swept dash bands pointing along the travel direction) - flowOpacity folds into the flow color's alpha CPU-side via new graph_model.applyHexOpacity — zero shader cost - flowDensity rides a new per-edge a_flowDensity attribute multiplying the per-mode pattern period (sparser = subtler); dot size and AA stay fixed; off-state emits neutral 1 since the shaders divide by it - styling panel: Flow Opacity + Flow Density slider rows, extended Flow Type vocabulary and tooltips - GLSL anchor tests extended to the new constants/varyings; pattern-constant co-change contract now covers chevron/comet
The reset-style button's tooltip promised a position reset that could never fire: after the first render every node's position is persisted, so changeLayout always took the animated path and pinned reset nodes to their current on-screen coords while excluding them from the tween. No original position is stored to restore to either. Remove the dead RESET_SELECTION_BUTTON_RESETS_POSITIONS flag, the position-deletion block in resetStyleForSelectedElements, and the misleading conditional tooltip. The button now only clears per-view custom styles, matching its static title. Per-workspace position save/restore is unaffected.
- Rename AGENTS.md to ARCHITECTURE.md and rewrite as an OSS-facing codebase map: Sigma v3 + graphology (was G6), corrected file inventory, SVG export, synced npm scripts; drop agent-session framing. - CHANGELOG: add Unreleased section (density heatmap, animated edge flow incl. comet/chevron, pie nodes, Dagre, SVG export, bubble-label knobs now honored, reset-style fix); remove the now-false no-op claim. - README: surface edge flow / heatmap / pie nodes, add SVG to exports, link ARCHITECTURE.md. - API.md: document edge arrow color/border and flow style fields. - SERVICE.md: genericize stale v1.13.1 startup banner.
- Add the six arrow color/border columns (Start/End Arrow Color, Arrow Border Color, Arrow Border Size) the Excel importer parses but the readme tab never documented. - Fix wrong default values against config.js: Label Font Size, Label Placement, Label Auto Rotate, Label Offset X, Label Color, Label Background Color, and both Arrow Type rows (default + enum). - Add a guard test asserting the readme tab documents exactly EXCEL_NODE_PROPERTIES / EXCEL_EDGE_PROPERTIES (fails on drift in either direction).
- Export now writes a top-level `version` stamp (was absent); loading a file saved by a newer app surfaces a soft notice via ui.info. Older and version-less files load unchanged (backward compatible). - The density-heatmap overlay (enabled + appearance settings), which lives on the adapter and not in cache.data, is now folded into the JSON export and restored after the graph renders on import. - Add StaticUtilities.isVersionNewer (dotted-numeric, rejects malformed / empty / non-numeric segments) and 29 tests covering the version stamp, heatmap round-trip, backward compatibility, and version comparison. - buildExportPayload is pure (does not mutate cache.data); version key is written last so the current app stamp always wins.
Creating a workspace with a super-linear layout (dagre/mds) on a large graph froze the page and let the user tinker mid-load. Three causes: - dagre/mds/radial/concentric ran synchronously on the main thread. Run them in a Blob worker (vendor_entry_layout_worker + embedded layout_worker_source), mirroring the FA2 force worker; the synchronous path stays as the node fallback and test seam. Main thread no longer freezes, so the overlay can paint and block. - the loading overlay was dropped mid-chain by a nested render's #postRefresh hideLoading(), exposing bubble-sync/hide-disconnected/ metrics and the layout worker. Add a counted hold/release on the overlay; addLayout/changeLayout pin it from showLoading and release it at the existing pre-tween point. hideLoading() is now idempotent. - keydown hotkeys bypassed the overlay (pointer-only barrier). Gate them on the new ui.isBusy(), which stays true while held. Also warn before kicking off an EXPENSIVE_LAYOUTS template above LAYOUT_NODE_WARNING_THRESHOLD (2000) nodes; declining cancels cleanly. Adds 12 tests (worker branch, busy-gate, hold/release, size guard).
8x exports silently failed on real GPUs: a ~14k-px WebGL framebuffer plus the same-size 2D composite canvas exhaust memory for large graphs, yielding blank or partial renders (edges survive, nodes drop) that evade the blank-canvas retry net. On HiDPI displays 8x clamped to ~5x anyway. - EXPORT_SCALES -> [1, 2, 4]; persisted 8 falls back to 1x via the existing includes() guard - add WEBGL_SAFE_SIDE_FRACTION (0.9) applied to the probed WebGL max side in toDataURL so even 4x never renders right at MAX_TEXTURE_SIZE, the regime where blank/partial/scanline corruption occurs - sync doc comments and the resolution-picker ladder
Fit each group's outline once at the ratio-1 reference scale and cache it in graph space, reprojecting per frame. Member enclosure is now zoom- invariant (nodes no longer flip in/out of a group as you zoom) and the hull always hugs the nodes, since outline and nodes ride the same camera transform. The camera is no longer in the outline cache key, so zooming never re-fits — only reprojects — removing the zoom lag and the mid-zoom phantom/misplaced artifacts. Repair self-intersecting bubblesets outlines via polygon self-union (new polygon-clipping dependency, vendored into graphology.bundle.mjs) instead of the earlier convex-hull fallback. This eliminates phantom chords/lobes and is essential now that a fixed fit no longer self-heals on zoom; a keep-last-good guard prevents a bad fit replacing a good one. Paint the bubble body above the node layer (afterLayer: "nodes") so groups stay visible at deep zoom. Removes the settle-debounce, hide-on- zoom, and ratio-bucket refit machinery, which are no longer needed.
Loading a JSON workspace restores each group's ManualMembers and renders the bubbles, but the selection-panel deselect toggles only build via bs.updateManualGroupStatus(), which the load path never called — so auto/louvain groups loaded un-deselectable. Mirror the post-layout sync after the load render. Add a regression test covering panel refresh on successful load and no refresh on empty data.
Bubble-group membership had three independent sources (filter/prop, manual selection, Louvain auto) whose UI surfaces drifted apart. - Add getEffectiveGroupMembers() as the single source of truth for a group's visible members (prop union manual, renderer's visibility filter). The status badge, styling-card enable state, and rendered outline all derive from it, so the displayed count can no longer diverge from what is highlighted. - Prompt for confirmation before Auto (Louvain) overwrites existing manual groups; cancel leaves the graph untouched. - Clearing a group (per-group badge and Clear all) now clears both manual members and filter/prop assignments, rebuilding the filter UI so the panel quadrants reflect the cleared state. - Fix the 'Add to group' quadrant button desync: resync it in updateSelectedNodesAndEdges (where cache.selectedNodes is authoritatively refreshed) instead of updateSelectedState, which ran on a stale selection. Adds bubble-group-seams (12) and selection-group-button-sync (2) tests.
Layouts previously only ran at workspace creation; within a workspace only partial 'arrange selection' was available. Add a whole-graph re-layout: - New Popup.layoutSelectDialog: algorithm picker pre-selected to the workspace's origin type, with a static overwrite warning and a dynamic perf warning for super-linear layouts (dagre/mds) on large graphs. - New GraphLayoutManager.relayoutWorkspace: recomputes all positions via the same setLayout/layout/persist/animated-transition pipeline as template creation, but stays on the current workspace and touches only positions (styles, filters, query, bubble membership untouched). - Stamp layoutType onto template- and clone-created workspaces so the picker defaults to the origin type and round-trips through JSON. - Add a relayout icon button to the workspace toolbar. - Tests: relayout-workspace (10) + popup-layout-select (11).
…teps - Header step: split into header actions (PNG/SVG export, dark mode) and toolbar pills; drop stale PNG-only and per-panel parentheticals - Workspaces: list all five icon buttons incl. new re-layout (🔄) - Filtering: rename Edit Mode → Details pill, show min/max inputs in mock-up - Add pie-chart nodes, edge flow, density heatmap to styling step - Selection panel: rewrite to match redesigned HUD (Tools/✕/lasso/style/ undo-redo, Add-to-group, Louvain 🧩 Auto + Clear all); fix non-red ✕ - Move Selection, Advanced Tools, and Styling steps up after the Canvas step - Collapse the selection panel during the Canvas step, restore on exit - Reorder closing keyboard-shortcut list to match the new tour flow
Wire two graphology-native layouts into the LAYOUT_INTERNALS vocabulary so they surface automatically in the create/re-layout dropdowns: - circlepack: d3-hierarchy circle packing, radius from each node's size attr - random: uniform scatter centered on the origin, extent scaling with order Both are O(n) geometric layouts (no expense guard) and reuse the existing pure (graph, spec) seam. Covered by the parametrized it.each plus dedicated non-overlap, size-scaling, and bounds tests. Also fix the simple-template readme tab: Start/End Arrow Border Size default FALSE -> 0 (number).
Reformat four outlier files (single quotes, expanded the minified excelData literal in io.js) to match the new .prettierrc. No behavioral change.
- Popup.confirm/prompt render messages via textContent, eliminating a file-borne XSS sink where layout/query names from loaded JSON reached innerHTML - query.js escapes untrusted main-group/sub-group/property/category names in the query-highlight overlay (new StaticUtilities.escapeHtml) - electron_app.js gates shell.openExternal behind an http(s)/mailto scheme allowlist - add escapeHtml regression tests
- selection.js: updateSelectionCache referenced a bare `cache` instead of `this.cache`, throwing ReferenceError whenever invoked - io.js, data_editor.js: replace unsafe obj.hasOwnProperty() with Object.prototype.hasOwnProperty.call(); declare ExcelJS global (vendored script tag) - color_scale_picker.js: [...x] || [] fallback was dead and threw on undefined; use [...(x || [])]
- drop the non-functional "Q" query button (console.log stub) and dead getPos() logger - remove completed-migration verification one-offs: phase4_browser_test.js, phase4_perf_probe.js, edge_markers_check.js (0 references, not wired into npm/CI) - remove ASSISTANT_QUESTIONS.md (not loaded by the app; orphaned doc)
…hygiene Tooling: - ESLint 9 flat config + Prettier (.prettierrc.json/.prettierignore) with lint/format scripts; eslint-config-prettier to avoid conflicts; per-file /* global */ directives for worker/browser-eval/ExcelJS scopes - @vitest/coverage-v8 + vitest.config.js; test:coverage script - .github/workflows/ci.yml runs lint + tests on PRs/main (perf gates as non-blocking job) Community & metadata: - SECURITY.md (disclosure policy for the Electron app + ingest service) and CODE_OF_CONDUCT.md - package.json: add license, repository, bugs fields Dependencies & attribution: - bump DOMPurify 3.4.1 -> 3.4.10 (re-vendored) to clear the advisory - regenerate THIRD_PARTY_NOTICES from the production tree via scripts/gen_third_party_notices.mjs (55 packages, all permissive; drop non-redistributed dev tooling) - gitignore coverage/
Fold the Unreleased section into a dated 1.15.0 entry and reorganize the Sigma-migration notes (layouts, overlays, file-format compatibility).
Cap the wheel-zoom FPS gate against the idle rAF ceiling (the treatment drag-pan already received) so rAF-pinned gates can't fail in environments that throttle below the raw limit. When the idle ceiling proves the runner has no usable GPU (< 30 fps), report the GPU-bound load gate without enforcing it. Strict gates are unchanged on real-GPU hardware.
The idle rAF ceiling is not a reliable no-GPU signal: a CI runner can clock requestAnimationFrame at 60 Hz while painting through SwiftShader at ~4 fps, so the prior idle-ceiling check passed the environment as real-GPU and the GPU-bound gates failed. Read UNMASKED_RENDERER_WEBGL directly and treat a software rasterizer (SwiftShader/llvmpipe/etc.) — or a sub-30 fps idle ceiling as a fallback — as a non-representative environment. There, the render/frame-cadence gates (load, first-interaction stall, wheel-zoom, drag-pan) are reported but not enforced; only the pure-JS 500-node select gate stays enforced. Full strict gates are unchanged on real-GPU hardware.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat/sigma-renderer → main (v1.15.0)
Summary
Migrates the rendering stack from AntV G6 5.x (canvas) to Sigma.js v3 (WebGL) on a graphology data model, then builds out layouts, communities, edge flow,
heatmap, and a UI rework on top. 68 commits, 131 files, +30.5k/−4k.
Why
G6's canvas renderer didn't scale. On the 6000-node / 9000-edge benchmark:
Distribution is also ≈0.9 MB smaller (vendored
g6.min.jsremoved).What's included
bubblesets-js),minimap, and PNG/SVG export — all ported to sigma.
graphology-metrics.Known degradations
Dashed edges render solid; polyline edges render curved (no off-the-shelf WebGL programs for either).
Compatibility
Existing graph files load unchanged — version-less 1.14.x JSON included. No saved-file format break.
Tests
774+ tests green across renderer, layouts, interactions, export, heatmap, and persistence; perf gates passing.